﻿
#region Imports

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using MammothMicro.SilverSocket.Protocol.System;
using MammothMicro.SilverSocket.Shared;
using NLog;


#endregion

namespace MammothMicro.SilverSocket.Server
{
    /// <summary>
    /// Receives and send messages.
    /// </summary>
    public abstract class SocketServer : IMessageServer
    {
        /// <summary>
        /// The _client registry.
        /// </summary>
        [SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1606:ElementDocumentationMustHaveSummaryText", 
            Justification = "Reviewed. Suppression is OK here.")] 
        private readonly ClientRegistry _clientRegistry = new ClientRegistry();

        /// <summary>
        /// Tcp _listener used to listen incoming socket messages.
        /// </summary>
        private readonly TcpListener _listener;

        /// <summary>
        /// Compress and decompress message 
        /// </summary>
        private readonly IMessageCompressor _messageCompressor;

        /// <summary>
        /// Encrypt and decrypt message 
        /// </summary>
        private readonly IMessageEncryptor _messageEncryptor;

        /// <summary>
        /// Serialize and deserialize messages.
        /// </summary>
        private readonly IMessageSerializer _messageSerializer;

        /// <summary>
        /// The _accept clients.
        /// </summary>
        protected bool _acceptClients;

        /// <summary>
        /// The _client version.
        /// </summary>
        protected string _clientVersion;

        /// <summary>
        /// The _compress data.
        /// </summary>
        private bool _compressData;

        /// <summary>
        /// The _encrypt data.
        /// </summary>
        private bool _encryptData;

        /// <summary>
        /// The _logger.
        /// </summary>
        protected Logger _logger = LogManager.GetCurrentClassLogger();

        /// <summary>
        /// Indicates that sever is stopped.
        /// </summary>
        private bool isStopped;

        /// <summary>
        /// Initializes a new instance of the <see cref="SocketServer"/> class.
        /// </summary>
        /// <param name="ipAddress">
        /// The ip Address.
        /// </param>
        /// <param name="port">
        /// The port.
        /// </param>
        /// <param name="messageSerializer">
        /// The message serializer.
        /// </param>
        /// <param name="messageCompressor">
        /// The message compressor.
        /// </param>
        /// <param name="messageEncryptor">
        /// The message encryptor.
        /// </param>
        public SocketServer(IPAddress ipAddress, int port, IMessageSerializer messageSerializer, IMessageCompressor messageCompressor, IMessageEncryptor messageEncryptor)
        {
            this._listener = new TcpListener(ipAddress, port);
            this._messageSerializer = messageSerializer;
            this._messageEncryptor = messageEncryptor;
            this._messageCompressor = messageCompressor;
        }

        #region Delegates and Events

        /// <summary>
        /// Occurs when new client has connected
        /// </summary>
        public event EventHandler<ClientStatusChangedEventArgs> ClientConnected;

        /// <summary>
        /// Occurs when client has disconnected
        /// </summary>
        public event EventHandler<ClientStatusChangedEventArgs> ClientDisconnected;

        /// <summary>
        /// Occurs when new message has been received
        /// </summary>
        public event EventHandler<MessageReceivedEventArgs> MessageReceived;

        public static event EventHandler<MessageReceivedEventArgs> TestEvent;
        #endregion

        /// <summary>
        /// Sets message compression
        /// </summary>
        public bool CompressData
        {
            get {return this._compressData;}

            set {this._compressData = value;}
        }

        /// <summary>
        /// Sets message encryption
        /// </summary>
        public bool EncryptData
        {
            get {return this._encryptData;}

            set {this._encryptData = value;}
        }

        /// <summary>
        /// Add new message types to the list
        /// </summary>
        /// <param name="knownTypes"></param>
        protected void UpdateKnownTypes(List<Type> knownTypes)
        {
            foreach (var item in knownTypes)
            {
                if (!_messageSerializer.KnownMessages.Contains(item))
                    _messageSerializer.KnownMessages.Add(item); 
            }
        }

        #region IMessageServer Members

        /// <summary>
        /// Sends message to client 
        /// </summary>
        /// <param name="clientId">
        /// </param>
        /// <param name="message">
        /// </param>
        public void SendMessageToClient(Guid clientId, DuplexCbMessage message)
        {
            this.SendMessageToClient(clientId, this.PrepareMessage(message));
        }

        /// <summary>
        /// Start listening for incoming messages.
        /// </summary>
        public void Start()
        {
            this._listener.Start();
            this._listener.BeginAcceptTcpClient(this.OnAcceptTcpClient, null);

            this._logger.Info("Socket server started listening for clients. Client version {0}.", this._clientVersion);
        }

        /// <summary>
        /// Stop listening for incoming messages.
        /// </summary>
        public void Stop()
        {
            this.isStopped = true;
            if (this._listener != null)
            {
                // TODO - Send close message to client
                this._listener.Stop();
            }

            foreach (var client in this._clientRegistry.GetAll())
            {
                client.TcpClient.Close();
            }

            this._logger.Info("Socket server stopped successfully.");
        }

        #endregion

        /// <summary>
        /// Main message sender
        /// </summary>
        /// <param name="clientId">
        /// </param>
        /// <param name="stream">
        /// </param>
        private void SendMessageToClient(Guid clientId, List<byte> stream)
        {
            Client client = this._clientRegistry.GetById(clientId);

            if (client != null)
            {
                var args = new SocketAsyncEventArgs();

                for (int i = 1023; i < stream.Count; i += 1023)
                {
                    stream.Insert(i, 0);
                    i++;
                }

                stream.Add(1);

                args.SetBuffer(stream.ToArray(), 0, stream.Count);

                try
                {
                    client.TcpClient.Client.SendAsync(args);
                }
                catch (Exception)
                {
                    this._clientRegistry.Remove(client.Id);
                    client.TcpClient.Close();
                    this.OnClientDisconnected(client.Id);
                }
            }

            // TODO - we should not be here, client is disconnected > handle it
        }

        /// <summary>
        /// Sends message to all connected clients
        /// </summary>
        /// <param name="message">
        /// </param>
        public void SendMessageToAllClients(DuplexCbMessage message)
        {
            List<byte> stream = this.PrepareMessage(message);
            foreach (var client in this._clientRegistry.GetAll())
            {
                this.SendMessageToClient(client.Id, stream);
            }
        }

        /// <summary>
        /// Serialize, encrypt, compress message
        /// </summary>
        /// <param name="message">
        /// </param>
        /// <returns>
        /// </returns>
        private List<byte> PrepareMessage(DuplexCbMessage message)
        {
            List<byte> stream = this._messageSerializer.Serialize(message);

            if (this._encryptData)
                stream = this._messageEncryptor.Encrypt(stream);

            if (this._compressData)
                stream = this._messageCompressor.Compress(stream);

            return stream;
        }


        /// <summary>
        /// The process message.
        /// </summary>
        /// <param name="stream">
        /// The stream.
        /// </param>
        /// <returns>
        /// </returns>
        private DuplexMessage ProcessMessage(byte[] stream)
        {
            List<byte> byteList = stream.ToList();
            if (this._compressData)
                byteList = this._messageCompressor.Decompress(byteList);

            if (this._encryptData)
                byteList = this._messageEncryptor.Decrypt(byteList);

            DuplexMessage obj = this._messageSerializer.Deserialize(byteList);
            return obj;
        }

        /// <summary>
        /// The begin receive client messages.
        /// </summary>
        /// <param name="clientMessageReceivedState">
        /// </param>
        private void BeginReceiveClientMessages(ClientMessageReceivedState clientMessageReceivedState)
        {
            try
            {
                clientMessageReceivedState.Client.TcpClient.Client.BeginReceive(
                    clientMessageReceivedState.MessagePacket, 
                    0, 
                    clientMessageReceivedState.MessagePacket.Length, 
                    SocketFlags.None, 
                    this.ClientMessageReceived, 
                    clientMessageReceivedState);
            }
            catch (SocketException e)
            {
                // TODO:  Add logger here.
                Console.WriteLine(e.Message);
            }
        }

        /// <summary>
        /// The client message received.
        /// </summary>
        /// <param name="ar">
        /// </param>
        private void ClientMessageReceived(IAsyncResult ar)
        {
            var state = (ClientMessageReceivedState) ar.AsyncState;
            try
            {
                int bytesRead = state.Client.TcpClient.Client.EndReceive(ar);

                if (bytesRead > 0)
                {
                    state.SerializedMessage.AddRange(state.MessagePacket.Take(bytesRead - 1));

                    if (state.MessagePacket[bytesRead - 1] > 0)
                    {
                        this.OnMessageReceived(state.Client.Id, state.SerializedMessage.ToArray());
                        state.SerializedMessage.Clear();
                    }
                }

                this.BeginReceiveClientMessages(state);
            }
            catch (SocketException)
            {
                this._clientRegistry.Remove(state.Client.Id);
                this.OnClientDisconnected(state.Client.Id);
            }
        }

        /// <summary>
        /// The on accept tcp client.
        /// </summary>
        /// <param name="asyncResult">
        /// </param>
        private void OnAcceptTcpClient(IAsyncResult asyncResult)
        {
            if (this.isStopped)
            {
                return;
            }

            this._listener.BeginAcceptTcpClient(this.OnAcceptTcpClient, null);

            TcpClient tcpClient = this._listener.EndAcceptTcpClient(asyncResult);

            var client = new Client(tcpClient);

            this.BeginReceiveClientMessages(
                new ClientMessageReceivedState
                    {
                        MessagePacket = new byte[1024], 
                        SerializedMessage = new List<byte>(), 
                        Client = client
                    });

            this._clientRegistry.Add(client);
            this.OnClientConnected(client.Id);
        }

        /// <summary>
        /// Invokes <see cref="ClientConnected"/> event.
        /// </summary>
        /// <param name="clientId">
        /// Client unique identifier
        /// </param>
        private void OnClientConnected(Guid clientId)
        {
            EventHandler<ClientStatusChangedEventArgs> handler = this.ClientConnected;
            if (handler != null)
            {
                handler(this, new ClientStatusChangedEventArgs(clientId));
            }

#if (DEBUG)
            this._logger.Info("Client successfully {0} connected", clientId);
#endif
        }

        /// <summary>
        /// Invokes <see cref="ClientDisconnected"/> event.
        /// </summary>
        /// <param name="clientId">
        /// Client unique identifier
        /// </param>
        private void OnClientDisconnected(Guid clientId)
        {
            EventHandler<ClientStatusChangedEventArgs> handler = this.ClientDisconnected;
            if (handler != null)
            {
                handler(this, new ClientStatusChangedEventArgs(clientId));
            }

#if (DEBUG)
            this._logger.Info("Client successfully {0} disconnected", clientId);
#endif
        }

        /// <summary>
        /// Invokes <see cref="MessageRecieved"/> event.
        /// </summary>
        /// <param name="clientId">
        /// Unique identifier of client, which sent the serializedMessage.
        /// </param>
        /// <param name="serializedMessage">
        /// Received serialized message.
        /// </param>
        private void OnMessageReceived(Guid clientId, byte[] serializedMessage)
        {
            var handler = MessageReceived;
            if (handler != null)
            {
                handler(
                    this, new MessageReceivedEventArgs(clientId.ToString(), this.ProcessMessage(serializedMessage)));
            }

            var handler1 = TestEvent;
            handler1(this, new MessageReceivedEventArgs(clientId.ToString(),this.ProcessMessage(serializedMessage)));
        }

        #region Private Class

        /// <summary>
        /// Inner object used as async method state.
        /// </summary>
        private class ClientMessageReceivedState
        {
            /// <summary>
            /// Client.
            /// </summary>
            public Client Client{get;set;}

            /// <summary>
            /// MessagePacket.
            /// </summary>
            public byte[] MessagePacket{get;set;}

            /// <summary>
            /// SerializedMessage
            /// </summary>
            public List<byte> SerializedMessage{get;set;}
        }

        #endregion    }
    }
}